Java — 类的加载过程
前言
今天在看 DCL 单例模式实现的时候,看到了作者分析 DCL 可能失效的问题,实际上讨论的是类的加载过程。比如执行 SingleTon singleTon = new SingleTon() ,这里看起来是一句代码,但实际上并不是一个原子操作,这句代码最终会被编译成多条汇编指令,它大致做了三件事情:
- 给 SingleTon 的实例分配内存;
- 调用SingleTon 的构造函数,初始化成员字段;
- 将 singleTon 对象指向分配的内存空间;
实际上还是有点懵,既然都挑事了,那就说明白点,枪一些博文(逃
类的加载过程概论
类从被加载到虚拟机内存开始,知道卸载出内存,它的生命周期包含了:加载,验证,准备,解析,初始化,使用和卸载七个阶段。
加载
将该类的 .class 文件中二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class 对象,用来封装类在方法区内的数据结构。在这个阶段,会执行类中声明的静态代码块。也就是说类中的静态块执行时是不需要等到类的初始化完成。
连接
类加载完成后就进入了类的连接阶段,在连接阶段,主要是将已经读到内存的类的二进制数据合并到虚拟机的运行时环境中去。连接阶段也分为三个过程:
验证
验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备
准备阶段是正式为 类变量 分配内存并设置其初始值的时候,内存都是被分配到方法区中。准备阶段不会分配类中实例变量的内存,实例变量的内存将会在对象实例化的时候随着对象一起分配到 Java 内存中。例如:public static int value = 123 ;在准备阶段 value 的初始值为 0,在初始化阶段才会变为123。而对于 static final 类型的变量,在准备阶段就会被赋值为正确的值。
解析
解析阶段是将符号引用转化为直接引用的过程。
符号引用是用一组符号来表示所引用的类和接口的全限定名、字段的名字和类型、方法的名字和类型,只要使用时可以无歧义的定位即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
直接引用是直接指向目标的指针、相对偏移量和一个能间接定位到目标的句柄。直接引用于虚拟机的内存布局是相关的,有了直接引用,说明引用的目标已经在内存中。
插播一下,为什么要将类、方法或者变量声明为 final 呢?
我们都知道,final 修饰的类不可继承,修饰的方法不可重写,修饰的变量不可重写赋值。但是说到优点,可能还真的说不上来。这就要提及的代码优化了,Java 编译器会寻找机会内联所有的 final 方法,内联对于提升 java 运行效率作用很大,平均能使性能提高 50%。
初始化
在这个阶段主要执行类的构造方法,并且为静态变量赋值为初始值,执行静态代码块。
如果你仔细读了上面的文章的话,你是不是发现了一个问题,前后不一致?静态代码块到底是在加载的时候被执行了还是在初始化时被执行了?实际是是在初始化的时候执行的。我看的这个博客是有点问题的。
初始化阶段是执行类构造器< clinit >() 方法的过程。该方法是由编译器自动收集类中的类变量的赋值动作和静态语句块中的语句合并产生。
虚拟机会保证类的初始化在多线程环境中被正确的加锁、同步,即如果多个线程同时去初始化一个类,那么只有一个类去执行这个类的< clinit > 方法,其他线程都要阻塞等待,直至活动线程执行完该方法。因此如果在一个类的 < clinit >() 方法中有耗时的操作,那就可能造成多个进程阻塞。不过其他线程虽然会阻塞,但是执行完 < clinit >() 方法的那个线程退出< clinit >() 后,其他线程就不会再次进入 < clinit >() 方法。因为在同一个类加载器下,一个类只会初始化一次。也就是说初始化之前的阶段可以有多个线程执行,而初始化阶段只能有一个线程执行。
类需要初始化的情况有以下几种:
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射
- 初始化一个类的子类
- Java 虚拟机启动时被标明启动类的类
剩下的阶段就不用说了,最后附上参考链接,其中第一篇有些说法有误,第三篇博客是对该问题的详细阐述和证明。
参考:
http://www.jianshu.com/p/6c91e07c293f
http://www.jianshu.com/p/79e0e9487b69
http://blog.csdn.net/jiese1990/article/details/40154329
http://www.cnblogs.com/ivanfu/archive/2012/02/12/2347817.html